﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace DeployGames.TweenEngine
{
    public abstract class BaseTween<T> : IBaseTween where T : IBaseTween, new()
    {
        // General
        private int step;
        private int repeatCnt;
        private bool isIterationStep;
        private bool isYoyo;

        // Timings
        
        protected float duration;
        private float repeatDelay;
        private float currentTime;
        private float deltaTime;
        private bool isStarted; // true when the object is started
        private bool isInitialized; // true after the delay
        private bool isFinished; // true when all repetitions are done
        private bool isKilled; // true if kill() was called
        private bool isPaused; // true if pause() was called

        // Misc
        private TweenCallback callback;
        private int callbackTriggers;
        private Object userData;

        public virtual void reset()
        {
            step = -2;
            repeatCnt = 0;
            isIterationStep = isYoyo = false;

            delay = duration = repeatDelay = currentTime = deltaTime = 0;
            isStarted = isInitialized = isFinished = isKilled = isPaused = false;

            callback = null;
            callbackTriggers = TweenCallback.COMPLETE;
            userData = null;

            IsAutoRemoveEnabled = IsAutoStartEnabled = true;
        }

        /// <summary>
        /// Builds and validates the object. Only needed if you want to finalize a
        /// tween or timeline without starting it, since a call to ".start()" also
        /// calls this method.
        /// </summary>
        /// <returns>The current object, for chaining instructions.</returns>
        public virtual T build()
        {
            return (T)(object)this;
        }

        /// <summary>
        ///  Starts or restarts the object unmanaged. You will need to take care of
        /// its life-cycle. If you want the tween to be managed for you, use a
        /// {@link TweenManager}.
        /// </summary>
        /// <returns>The current object, for chaining instructions.</returns>
        public virtual T start()
        {
            build();
            currentTime = 0;
            isStarted = true;
            return (T)(object)this;
        }

        /// <summary>
        /// Convenience method to add an object to a manager. Its life-cycle will be
        /// handled for you. Relax and enjoy the animation.
        /// </summary>
        /// <param name="manager"></param>
        /// <returns></returns>
        public T start(TweenManager manager)
        {
            manager.add(this);
            return (T)(object)this;
        }

        /// <summary>
        /// Adds a delay to the tween or timeline.
        /// </summary>
        /// <param name="delay">delay A duration.</param>
        /// <returns>The current object, for chaining instructions.</returns>
        public T Delay(float delay)
        {
            this.delay += delay;
            return (T)(object)this;
        }

        /// <summary>
        /// Kills the tween or timeline. If you are using a TweenManager, this object
        /// will be removed automatically.
        /// </summary>
        public void kill()
        {
            isKilled = true;
        }

        /// <summary>
        /// Stops and resets the tween or timeline, and sends it to its pool, for
        /// later reuse. Note that if you use a {@link TweenManager}, this method
        /// is automatically called once the animation is finished.
        /// </summary>
        public virtual void free() { }

        /// <summary>
        /// Pauses the tween or timeline. Further update calls won't have any effect.
        /// </summary>
        public virtual void pause()
        {
            isPaused = true;
        }

        /// <summary>
        /// Resumes the tween or timeline. Has no effect is it was no already paused.
        /// </summary>
        public virtual void resume()
        {
            isPaused = false;
        }

        /// <summary>
        /// Repeats the tween or timeline for a given number of times.
        /// </summary>
        /// <param name="count">The number of repetitions. For infinite repetition, 
        /// use Tween.INFINITY, or a negative number.</param>
        /// <param name="delay">A delay between each iteration.</param>
        /// <returns>The current tween or timeline, for chaining instructions.</returns>
        public virtual T repeat(int count, float delay)
        {
            if (isStarted) throw new Exception("You can't change the repetitions of a tween or timeline once it is started");
            repeatCnt = count;
            repeatDelay = delay >= 0 ? delay : 0;
            isYoyo = false;
            return (T)(object)this;
        }

        /// <summary>
        /// Repeats the tween or timeline for a given number of times.
        /// Every two iterations, it will be played backwards.        
        /// </summary>
        /// <param name="count">The number of repetitions. For infinite repetition,
        /// use Tween.INFINITY, or '-1'.</param>
        /// <param name="delay">A delay before each repetition.</param>
        /// <returns>The current tween or timeline, for chaining instructions.</returns>
        public T repeatYoyo(int count, float delay)
        {
            if (isStarted) throw new Exception("You can't change the repetitions of a tween or timeline once it is started");
            repeatCnt = count;
            repeatDelay = delay >= 0 ? delay : 0;
            isYoyo = true;
            return (T)(object)this;
        }

        /// <summary>
        /// Sets the callback. By default, it will be fired at the completion of the
        /// tween or timeline (event COMPLETE). If you want to change this behavior
        /// and add more triggers, use the {@link setCallbackTriggers()} method.
        /// </summary>
        /// <param name="callback">The Callback to set.</param>
        /// <returns>The current tween or timeline, for chaining instructions.</returns>
        public T setCallback(TweenCallback callback)
        {
            this.callback = callback;
            return (T)(object)this;
        }

        /// <summary>
        /// Changes the triggers of the callback. The available triggers, listed as
        /// members of the {@link TweenCallback} interface, are:
        /// <p/>
        ///
        /// <b>BEGIN</b>: right after the delay (if any)<br/>
        /// <b>START</b>: at each iteration beginning<br/>
        /// <b>END</b>: at each iteration ending, before the repeat delay<br/>
        /// <b>COMPLETE</b>: at last END event<br/>
        /// <b>BACK_BEGIN</b>: at the beginning of the first backward iteration<br/>
        /// <b>BACK_START</b>: at each backward iteration beginning, after the repeat delay<br/>
        /// <b>BACK_END</b>: at each backward iteration ending<br/>
        /// <b>BACK_COMPLETE</b>: at last BACK_END event
        /// <p/>
        ///
        /// <pre> {@code
        /// forward :      BEGIN                                   COMPLETE
        /// forward :      START    END      START    END      START    END
        /// |--------------[XXXXXXXXXX]------[XXXXXXXXXX]------[XXXXXXXXXX]
        /// backward:      bEND  bSTART      bEND  bSTART      bEND  bSTART
        /// backward:      bCOMPLETE                                 bBEGIN
        /// }</pre>
        /// </summary>
        /// <param name="flags">one or more triggers, separated by the '|' operator.</param>
        /// <returns>The current tween or timeline, for chaining instructions.</returns>
        public T setCallbackTriggers(int flags)
        {
            this.callbackTriggers = flags;
            return (T)(object)this;
        }

        /// <summary>
        /// Attaches an object to this tween or timeline. It can be useful in order
        /// to retrieve some data from a TweenCallback.
        /// </summary>
        /// <param name="data">Any kind of object.</param>
        /// <returns>The current tween or timeline, for chaining instructions.</returns>
        public T setUserData(Object data)
        {
            userData = data;
            return (T)(object)this;
        }

        /// <summary>
        /// Gets the delay of the tween or timeline. Nothing will happen before this delay.
        /// </summary>
        /// <returns></returns>
        public float getDelay()
        {
            return delay;
        }

        /// <summary>
        /// Gets the duration of a single iteration.
        /// </summary>
        /// <returns></returns>
        public float getDuration()
        {
            return duration;
        }

        /// <summary>
        /// Gets the number of iterations that will be played.
        /// </summary>
        /// <returns></returns>
        public int getRepeatCount()
        {
            return repeatCnt;
        }

        /// <summary>
        /// Gets the delay occuring between two iterations.
        /// </summary>
        /// <returns></returns>
        public float getRepeatDelay()
        {
            return repeatDelay;
        }

        /// <summary>
        /// Returns the complete duration, including initial delay and repetitions.
        /// The formula is as follows:
        /// <pre>
        /// fullDuration = delay + duration + (repeatDelay + duration) /// repeatCnt
        /// </pre>
        /// </summary>
        /// <returns></returns>
        public float getFullDuration()
        {
            if (repeatCnt < 0) return -1;
            return delay + duration + (repeatDelay + duration) * repeatCnt;
        }

        /// <summary>
        /// Gets the attached data, or null if none.
        /// </summary>
        /// <returns></returns>
        public Object getUserData()
        {
            return userData;
        }

        /// <summary>
        /// Gets the id of the current step. Values are as follows:<br/>
        /// <ul>
        /// <li>even numbers mean that an iteration is playing,<br/>
        /// <li>odd numbers mean that we are between two iterations,<br/>
        /// <li>-2 means that the initial delay has not ended,<br/>
        /// <li>-1 means that we are before the first iteration,<br/>
        /// <li>repeatCount///2 + 1 means that we are after the last iteration
        /// </summary>
        /// <returns></returns>
        public int getStep()
        {
            return step;
        }

        /// <summary>
        /// Gets the local time.
        /// </summary>
        /// <returns></returns>
        public float getCurrentTime()
        {
            return currentTime;
        }

        /// <summary>
        /// Returns true if the tween or timeline has been started.
        /// </summary>
        /// <returns></returns>
        public bool IsStarted()
        {
            return isStarted;
        }

        /// <summary>
        /// Returns true if the tween or timeline has been initialized. Starting
        /// values for tweens are stored at initialization time. This initialization
        /// takes place right after the initial delay, if any.
        /// </summary>
        /// <returns></returns>
        public bool IsInitialized()
        {
            return isInitialized;
        }

        /// <summary>
        /// Returns true if the tween is finished (i.e. if the tween has reached
        /// its end or has been killed). If you don't use a TweenManager, you may
        /// want to call {@link free()} to reuse the object later.
        /// </summary>
        /// <returns></returns>
        public bool IsFinished()
        {
            return isFinished || isKilled;
        }

        /// <summary>
        /// Returns true if the iterations are played as yoyo. Yoyo means that
        /// every two iterations, the animation will be played backwards.
        /// </summary>
        /// <returns></returns>
        public bool IsYoyo()
        {
            return isYoyo;
        }

        /// <summary>
        /// Returns true if the tween or timeline is currently paused.
        /// </summary>
        /// <returns></returns>
        public bool IsPaused()
        {
            return isPaused;
        }

        protected abstract void forceStartValues();
        protected abstract void forceEndValues();
        protected abstract bool containsTarget(Object target);
        protected abstract bool containsTarget(Object target, int tweenType);

        protected virtual void initializeOverride()
        {
        }

        protected virtual void updateOverride(int step, int lastStep, bool isIterationStep, float delta)
        {
        }

        protected void forceToStart()
        {
            currentTime = -delay;
            step = -1;
            isIterationStep = false;
            if (isReverse(0)) forceEndValues();
            else forceStartValues();
        }

        protected void forceToEnd(float time)
        {
            currentTime = time - getFullDuration();
            step = repeatCnt * 2 + 1;
            isIterationStep = false;
            if (isReverse(repeatCnt * 2)) forceStartValues();
            else forceEndValues();
        }

        protected void callCallback(int type)
        {
            if (callback != null && (callbackTriggers & type) > 0) callback.onEvent(type, this);
        }

        protected bool isReverse(int step)
        {
            return isYoyo && Math.Abs(step % 4) == 2;
        }

        protected bool isValid(int step)
        {
            return (step >= 0 && step <= repeatCnt * 2) || repeatCnt < 0;
        }

        protected void killTarget(Object target)
        {
            if (containsTarget(target)) kill();
        }

        protected void killTarget(Object target, int tweenType)
        {
            if (containsTarget(target, tweenType)) kill();
        }

        /// <summary>
        /// Updates the tween or timeline state. <b>You may want to use a
        /// TweenManager to update objects for you.</b>
        ///
        /// Slow motion, fast motion and backward play can be easily achieved by
        /// tweaking this delta time. Multiply it by -1 to play the animation
        /// backward, or by 0.5 to play it twice slower than its normal speed.
        /// </summary>
        /// <param name="delta"></param>
        public void update(float delta)
        {
            if (!isStarted || isPaused || isKilled) return;

            deltaTime = delta;

            if (!isInitialized)
            {
                initialize();
            }

            if (isInitialized)
            {
                testRelaunch();
                updateStep();
                testCompletion();
            }

            currentTime += deltaTime;
            deltaTime = 0;
        }

        private void initialize()
        {
            if (currentTime + deltaTime >= delay)
            {
                initializeOverride();
                isInitialized = true;
                isIterationStep = true;
                step = 0;
                deltaTime -= delay - currentTime;
                currentTime = 0;
                callCallback(TweenCallback.BEGIN);
                callCallback(TweenCallback.START);
            }
        }

        private void testRelaunch()
        {
            if (!isIterationStep && repeatCnt >= 0 && step < 0 && currentTime + deltaTime >= 0)
            {
                Debug.Assert(step == -1);
                isIterationStep = true;
                step = 0;
                float delta = 0 - currentTime;
                deltaTime -= delta;
                currentTime = 0;
                callCallback(TweenCallback.BEGIN);
                callCallback(TweenCallback.START);
                updateOverride(step, step - 1, isIterationStep, delta);

            }
            else if (!isIterationStep && repeatCnt >= 0 && step > repeatCnt * 2 && currentTime + deltaTime < 0)
            {
                Debug.Assert(step == repeatCnt * 2 + 1);
                isIterationStep = true;
                step = repeatCnt * 2;
                float delta = 0 - currentTime;
                deltaTime -= delta;
                currentTime = duration;
                callCallback(TweenCallback.BACK_BEGIN);
                callCallback(TweenCallback.BACK_START);
                updateOverride(step, step + 1, isIterationStep, delta);
            }
        }

        private void updateStep()
        {
            while (isValid(step))
            {
                if (!isIterationStep && currentTime + deltaTime <= 0)
                {
                    isIterationStep = true;
                    step -= 1;

                    float delta = 0 - currentTime;
                    deltaTime -= delta;
                    currentTime = duration;

                    if (isReverse(step)) forceStartValues(); else forceEndValues();
                    callCallback(TweenCallback.BACK_START);
                    updateOverride(step, step + 1, isIterationStep, delta);

                }
                else if (!isIterationStep && currentTime + deltaTime >= repeatDelay)
                {
                    isIterationStep = true;
                    step += 1;

                    float delta = repeatDelay - currentTime;
                    deltaTime -= delta;
                    currentTime = 0;

                    if (isReverse(step)) forceEndValues(); else forceStartValues();
                    callCallback(TweenCallback.START);
                    updateOverride(step, step - 1, isIterationStep, delta);

                }
                else if (isIterationStep && currentTime + deltaTime < 0)
                {
                    isIterationStep = false;
                    step -= 1;

                    float delta = 0 - currentTime;
                    deltaTime -= delta;
                    currentTime = 0;

                    updateOverride(step, step + 1, isIterationStep, delta);
                    callCallback(TweenCallback.BACK_END);

                    if (step < 0 && repeatCnt >= 0) callCallback(TweenCallback.BACK_COMPLETE);
                    else currentTime = repeatDelay;

                }
                else if (isIterationStep && currentTime + deltaTime > duration)
                {
                    isIterationStep = false;
                    step += 1;

                    float delta = duration - currentTime;
                    deltaTime -= delta;
                    currentTime = duration;

                    updateOverride(step, step - 1, isIterationStep, delta);
                    callCallback(TweenCallback.END);

                    if (step > repeatCnt * 2 && repeatCnt >= 0) callCallback(TweenCallback.COMPLETE);
                    currentTime = 0;

                }
                else if (isIterationStep)
                {
                    float delta = deltaTime;
                    deltaTime -= delta;
                    currentTime += delta;
                    updateOverride(step, step, isIterationStep, delta);
                    break;

                }
                else
                {
                    float delta = deltaTime;
                    deltaTime -= delta;
                    currentTime += delta;
                    break;
                }
            }
        }

        private void testCompletion()
        {
            isFinished = repeatCnt >= 0 && (step > repeatCnt * 2 || step < 0);
        }

        public bool IsAutoRemoveEnabled { get; set; }

        public bool IsAutoStartEnabled { get; set; }

        void IBaseTween.start()
        {
            this.start();
        }


        bool IBaseTween.containsTarget(object target)
        {
            return this.containsTarget(target);
        }

        bool IBaseTween.containsTarget(object target, int tweenType)
        {
            return this.containsTarget(target, tweenType);
        }

        void IBaseTween.killTarget(object target)
        {
            this.killTarget(target);
        }

        void IBaseTween.killTarget(object target, int tweenType)
        {
            this.killTarget(target, tweenType);
        }

        public float delay { get; set; }

        void IBaseTween.build()
        {
            this.build();
        }

        void IBaseTween.forceToStart()
        {
            this.forceToStart();
        }

        void IBaseTween.forceToEnd(float time)
        {
            this.forceToStart();
        }
    }
}
